[AWS CDK] CodePipelineのソース元を色々指定してみました(CodeCommitとか、Githubとか、S3 Bucketとか、BacklogのGitとか)
1 はじめに
CX事業本部の平内(SIN)です。
CodePipelineでは、各種のソースが利用可能です。 今回は、AWS CDK(AWS Cloud Development Kit)で、CodePipelineのソース元を色々変えて記述要領を確認してみました。
2 サンプル
最初に、本記事の基準となるサンプルコードです。
更新されたコードでLambdaを更新します。
#!/usr/bin/env node import cdk = require('@aws-cdk/core'); import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as iam from '@aws-cdk/aws-iam'; //**************************************************** */ // buildspec.yamの中から、functionNameに対してdeployされる想定 const stage = "dev"; // "stg","prd" const functionName = stage + '-myFunction' //**************************************************** */ export class CdkCodePipelineSampleStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); //**************************************************** */ // 1. プロジェクトの生成 //**************************************************** */ const project = new codebuild.PipelineProject(this, 'project', { projectName: 'myProject-' + stage, environment: { // 環境変数(関数名及び、ステージ)をbuildspec.ymlに送るってデプロイする environmentVariables: { FUNCTION_NAME: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: functionName, }, STAGE: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: stage, } }, } }); // buildspc.ymlからLambdaをupdateするため、パーミッションを追加 project.addToRolePolicy(new iam.PolicyStatement({ resources: [`arn:aws:lambda:${this.region}:${this.account}:function:${functionName}`], actions: ['lambda:UpdateFunctionCode', 'lambda:UpdateFunctionConfiguration',] } )); // パイプラインの生成 const sourceOutput = new codepipeline.Artifact(); //**************************************************** */ // 2. ソースアクションの生成 //**************************************************** */ const repositoryName = 'CdkCodePipelineSample'; const branch = 'develop'; // 'release','master'; const repo = codecommit.Repository.fromRepositoryName(this, "repo", repositoryName) as codecommit.Repository; const sourceAction = new codepipeline_actions.CodeCommitSourceAction ({ actionName: 'CodeCommit', repository: repo, branch: branch, output: sourceOutput, }); //**************************************************** */ // 3. ビルドアクションの生成 //**************************************************** */ const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: 'CodeBuild', project, input: sourceOutput, outputs: [new codepipeline.Artifact()] }); //**************************************************** */ // 4. パイプラインの生成 //**************************************************** */ new codepipeline.Pipeline(this, 'pipeline', { pipelineName: 'myPipeline-' + stage, stages: [ { stageName: 'Source', actions: [ sourceAction ], }, { stageName: 'Build', actions: [ buildAction ], } ] }) } } const app = new cdk.App(); new CdkCodePipelineSampleStack(app, 'CdkCodePipelineSampleStack');
デプロイは、buildspec.ymlでaws lambda update-function-codeされています。
buildspec.yml
version: 0.2 env: variables: DESCRIPTION: sample function RUN_TIME: nodejs10.x MEMORY: 128 TIMEOUT: 5 HANDLER: index.handler ENV: TZ=Asia/Tokyo,NODE_ENV= # NODE_ENV=$STAGE ZIP_FILE: /tmp/upload.zip phases: pre_build: commands: - yarn global add typescript - yarn install - build: commands: - cd src - tsc - npm test - zip -r -q ${ZIP_FILE} * - aws lambda update-function-code --function-name $FUNCTION_NAME --zip-file fileb://$ZIP_FILE --publish - aws lambda update-function-configuration --function-name $FUNCTION_NAME --environment Variables={$ENV$STAGE} --memory-size $MEMORY --runtime $RUN_TIME --description "$DESCRIPTION" --timeout $TIMEOUT --handler $HANDLER post_build: commands: - echo Deploy completed
3 Code Commit
上記の基準サンプルは、ソース元が、Code Commitになっています。 codepipeline_actions.CodeCommitSourceAction()で、リポジトリ名とブランチ名を指定するだけで。
//**************************************************** */ // 2. ソースアクションの生成 //**************************************************** */ const repositoryName = 'myRepository'; const branch = 'develop'; // 'release','master'; const repo = codecommit.Repository.fromRepositoryName(this, "repo", repositoryName) as codecommit.Repository; const sourceAction = new codepipeline_actions.CodeCommitSourceAction ({ actionName: 'CodeCommit', repository: repo, branch: branch, output: sourceOutput, });
4 Github
ソース元がGitHubになる場合は、codepipeline_actions.GitHubSourceAction()を使用します。
パラメータには、リポジトリ名、ブランチ名の他に、GiutHubのオーナー名及び、認証トークンが必要になります。
import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; // ・・・略・・・ //**************************************************** */ // 2. ソースアクションの生成 //**************************************************** */ const repositoryName = 'CdkCodePipelineSample'; const owner = 'HIRAUCHI'; const oauthToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; const branch = 'develop'; // 'release','master'; const sourceAction = new codepipeline_actions.GitHubSourceAction ({ actionName: 'Github', owner: owner, repo: repositoryName, branch: branch, oauthToken: cdk.SecretValue.plainText(oauthToken), trigger: codepipeline_actions.GitHubTrigger.POLL // 'WEBHOOK', 'NONE' output: sourceOutput, });
なお、triggerでWebhookも指定可能です。
trigger: codepipeline_actions.GitHubTrigger.WEBHOOK
5 S3のバケット
codepipeline_actions.S3SourceAction()で、S3バケットをソース元に指定できます。バケットには、バージョニングの設定が必須です。
import * as s3 from '@aws-cdk/aws-s3'; // ・・・略・・・ //**************************************************** */ // 2. ソースアクションの生成 //**************************************************** */ const sourceBucket = new s3.Bucket(this, 'sourceBucket', { bucketName: 'source-bucket-' + this.account, versioned: true, // バージョニングが必須 }); const sourceAction = new codepipeline_actions.S3SourceAction({ actionName: 'S3', bucket: sourceBucket, bucketKey: 'upload.zip', output: sourceOutput, });
6 ECR(参考)
CodePipelineでは、ECRをソース元にすることも可能ですが、今回は、Lambdaのデプロイがサンプルとなっているため、参考にコード例 のみ紹介されて頂きます。
import * as ecr from '@aws-cdk/aws-ecr'; // ・・・略・・・ //**************************************************** */ // 2. ソースアクションの生成 //**************************************************** */ const repositoryName = 'CdkCodePipelineSample'; const sourceAction = new codepipeline_actions.EcrSourceAction ({ actionName: 'ECR', repository: repositoryName, imageTag: imageTag, output: sourceOutput, });
7 BacklogのGit
BacklogのGitでは、更新時にWebhookを仕掛けることができます。このWebhookでLambdaを起動し、対象のコードをcloneしてS3にアップロードします。S3にアップロードされた後は、上記の「S3のバケット」の仕組みをそのまま使用します。
(1) Webhook
最初に、BacklogのGitでWebフックを設定します。 指定するURLは、CDKのdeployで最後に表示される、API GatewayのEndpointをコピーします。
(2) Lambda
API Gatewayの後ろに配置するLambdaでは、BacklogのGitから送られてきた情報に基づいて、コードをgit cloneし、圧縮して、 S3バケットにアップロードしています。
珍しくPythonで書いているのですが、理由は以下のとおりです。
- BacklogのGitは、httpsでダウンロードできない
- GitコマンドをLambda上で軽易に使えるライブラリがpythonに用意されている(porcelain)
- Lambda上でzip圧縮を行えるライブラリがpythonに用意されている(shutil)
JavaScriptでLambdaからgit cloneするのは、予想以上にハードルが高かったです・・・
import json import urllib.parse import os import tempfile import shutil import boto3 from dulwich import porcelain BUCKET_NAME = os.environ['BUCKET_NAME'] ZIP_FILE_NAME = os.environ['ZIP_FILE_NAME'] USER = os.environ['USER'] PASS = os.environ['PASS'] REPOSITORY = os.environ['REPOSITORY'] BLANCH = os.environ['BLANCH'] def lambda_handler(event, context): payloadStr = urllib.parse.unquote(event["body"][8:]) # event["body"] payload="xxxxxx" payload = json.loads(payloadStr) repository = payload["repository"]["name"] url = payload["repository"]["url"] branch = payload["ref"][11:] # refs/heads/master" print(f"repository:{repository} branch:{branch} uri:{url}") if repository == REPOSITORY and branch == BLANCH: # gitパスの生成 site = urllib.parse.urlparse(url) userStr = urllib.parse.quote(USER) passStr = urllib.parse.quote(PASS) uri = site.scheme +"://" + userStr + ":" + passStr +"@" + site.netloc + site.path + ".git" # 作業ディレクトリの生成 tmpDir = tempfile.mkdtemp() # clone/zip/upload try: porcelain.clone(uri, tmpDir) print("git clone success") zipFileName = tmpDir+ '/' + os.path.splitext(ZIP_FILE_NAME)[0] shutil.make_archive(zipFileName, 'zip', tmpDir ) print("zip success") s3 = boto3.client('s3') s3.upload_file(zipFileName + '.zip', BUCKET_NAME, ZIP_FILE_NAME) print("s3 upload success") except Exception as e: print("ERROR" + e) # 後始末 shutil.rmtree(tmpDir)
(3) AWS CDK
最後に、S3をソース元としたCodePipelineと、Webhook用のAPI GatewayとLambdaを追加したAWS CDKの全コードです。
#!/usr/bin/env node import cdk = require('@aws-cdk/core'); import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as lambda from '@aws-cdk/aws-lambda'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as apigateway from '@aws-cdk/aws-apigateway'; //**************************************************** */ // buildspec.yamの中から、functionNameに対してdeployされる想定 const stage = "dev"; // "stg","prd" const functionName = stage + '-myFunction' //**************************************************** */ // BacklogのGit情報 const repositoryName = 'test-project'; const branch = 'develop'; const user = 'user@example.jp'; const pass = 'xxxxxx'; export class CdkCodePipelineWithBacklogStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const bucketName = 'source-bucket-' + this.account; const zipFileName = 'upload.zip'; //**************************************************** */ // プロジェクトの生成 //**************************************************** */ const project = new codebuild.PipelineProject(this, 'project', { projectName: 'myProject-' + stage, environment: { // 環境変数(関数名及び、ステージ)をbuildspec.ymlに送るってデプロイする environmentVariables: { FUNCTION_NAME: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: functionName, }, STAGE: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: stage, } }, } }); // buildspc.ymlからLambdaをupdateするため、パーミッションを追加 project.addToRolePolicy(new iam.PolicyStatement({ resources: [`arn:aws:lambda:${this.region}:${this.account}:function:${functionName}`], actions: ['lambda:UpdateFunctionCode', 'lambda:UpdateFunctionConfiguration',] } )); // パイプラインの生成 const sourceOutput = new codepipeline.Artifact(); //**************************************************** */ // ソースアクションの生成 //**************************************************** */ const sourceBucket = new s3.Bucket(this, 'sourceBucket', { bucketName: bucketName, versioned: true, // バージョニングが必須 }); const sourceAction = new codepipeline_actions.S3SourceAction({ actionName: 'S3', bucket: sourceBucket, bucketKey: zipFileName, output: sourceOutput, }); //**************************************************** */ // ビルドアクションの生成 //**************************************************** */ const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: 'CodeBuild', project, input: sourceOutput, outputs: [new codepipeline.Artifact()] }); //**************************************************** */ // パイプラインの生成 //**************************************************** */ new codepipeline.Pipeline(this, 'pipeline', { pipelineName: 'myPipeline-' + stage, stages: [ { stageName: 'Source', actions: [ sourceAction ], }, { stageName: 'Build', actions: [ buildAction ], } ] }) //**************************************************** */ // Webhook用の Lambda //**************************************************** */ const getFunction = new lambda.Function(this, "get-function",{ functionName: this.stackName + "-getSourceFromBacklog", code: lambda.Code.asset("lambda"), handler:"lambda_function.lambda_handler", runtime: lambda.Runtime.PYTHON_3_7, timeout: cdk.Duration.seconds(120), environment: { "BUCKET_NAME": bucketName, "ZIP_FILE_NAME": zipFileName, "REPOSITORY": repositoryName, "BLANCH": branch, "USER": user, "PASS": pass, } }) getFunction.addToRolePolicy(new iam.PolicyStatement({ resources: [`arn:aws:s3:::${bucketName}/${zipFileName}`], actions: ['s3:putObject'] } )); //**************************************************** */ // Webhook用の API Gateway //**************************************************** */ const api = new apigateway.RestApi(this, "api"); const lambdaIntegration = new apigateway.LambdaIntegration(getFunction); api.root.addMethod("POST",lambdaIntegration); } } const app = new cdk.App(); new CdkCodePipelineWithBacklogStack(app, 'CdkCodePipelineWithBacklogStack');
8 最後に
今回、CodePipelineのソース元を色々記述してみました。
Lambdaのデプロイが目的だとしても、他にもソース元が変化する可能性はあると思います。しかし、上記の例を参考にすれば、なんとかなりそうな予感がします。